package de.mmis.core.pojop;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.jar.JarFile;

import org.apache.log4j.Logger;

import de.mmis.core.base.ClassFinder;
import de.mmis.core.base.Position;
import de.mmis.core.base.Type;
import de.mmis.core.base.abstracttree.Tree;
import de.mmis.core.base.abstracttree.serializer.DeserializableException;
import de.mmis.core.base.gui.DeviceControlComponent;
import de.mmis.core.base.gui.DeviceRepresentationComponent;
import de.mmis.core.base.informationprovider.DeviceInformationProvider;
import de.mmis.core.base.informationprovider.PropagatingDeviceInformationProvider;

/**
 * {@link DeviceInformationProvider} wrapper class using information provided by
 * a {@link NetworkProxy}. Information not provided will be taken from the
 * parent {@link DeviceInformationProvider} (if any).
 * 
 * @author Christian Koch
 * 
 */
public class NetworkPublisherInformationProvider extends
		PropagatingDeviceInformationProvider {

	private static Logger LOGGER = Logger
			.getLogger(NetworkPublisherInformationProvider.class);

	// private final String host;
	// private final int port;
	private Socket sock;

	private File tempFile;
	private JarFile tempJarFile;
	private ClassLoader classLoader;
	private ClassLoader parentClassLoader;

	private Class<?>[] controlComponentClasses = null;
	private Class<?>[] representationComponentClasses = null;

	private Map<String, Object> properties = null;

	public NetworkPublisherInformationProvider(Socket sock) {
		this(sock, null, NetworkPublisherInformationProvider.class
				.getClassLoader(), false);
	}

	public NetworkPublisherInformationProvider(Socket sock,
			DeviceInformationProvider parent) {
		this(sock, parent, NetworkPublisherInformationProvider.class
				.getClassLoader(), false);
	}

	public NetworkPublisherInformationProvider(Socket sock,
			DeviceInformationProvider parent, ClassLoader parentClassLoader) {
		this(sock, parent, parentClassLoader, false);
	}

	/*
	 * public NetworkPublisherInformationProvider(String host, int port,
	 * DeviceInformationProvider parent, ClassLoader parentClassLoader, boolean
	 * getJarData) throws UnknownHostException, IOException { this(new
	 * Socket(host, port), parent, parentClassLoader, getJarData); }
	 */

	public NetworkPublisherInformationProvider(Socket sock,
			DeviceInformationProvider parent, ClassLoader parentClassLoader,
			boolean getJarData) {
		super(parent, parentClassLoader);

		this.sock = sock;
		this.parentClassLoader = parentClassLoader;

		this.classLoader = null;

		if (sock != null && sock.isConnected())
			try {
				properties = NetworkProxy.getProperties(this.sock);
			} catch (IOException e) {
				LOGGER.error("error while getting device properties map", e);
				sock = null;
			} catch (DeserializableException e) {
				LOGGER.error("error while getting device properties map", e);
			}
		if (sock != null && sock.isConnected() && getJarData)
			try {
				this.tempFile = File.createTempFile("temp", ".jar");
				this.tempFile.deleteOnExit();

				FileOutputStream out = new FileOutputStream(this.tempFile);
				NetworkProxy.getJarData(sock, out);
				out.close();

				this.tempJarFile = new JarFile(this.tempFile);

				this.classLoader = new URLClassLoader(new URL[] { this.tempFile
						.toURI().toURL() }, parentClassLoader);

				this.controlComponentClasses = ClassFinder.findSubclasses(
						DeviceControlComponent.class, this.tempJarFile,
						this.classLoader);

				this.representationComponentClasses = ClassFinder
						.findSubclasses(DeviceRepresentationComponent.class,
								this.tempJarFile, this.classLoader);

			} catch (IOException e) {
				LOGGER.error(
						"ERROR while trying to get NetworkPublisher jar data from "
								+ sock.getInetAddress() + ":" + sock.getPort(),
						e);
				sock = null;
				return;
			}
	}

	@Override
	public Class<?>[] getControlComponentClasses() {
		Class<?>[] classes = super.getControlComponentClasses();

		List<Class<?>> ret = new ArrayList<Class<?>>();

		if (classes != null)
			for (Class<?> c : classes)
				ret.add(c);

		if (controlComponentClasses != null)
			for (Class<?> c : controlComponentClasses)
				ret.add(c);

		return ret.toArray(new Class<?>[ret.size()]);
	}

	@Override
	public Class<?>[] getRepresentationComponentClasses() {
		Class<?>[] classes = super.getRepresentationComponentClasses();

		List<Class<?>> ret = new ArrayList<Class<?>>();

		if (classes != null)
			for (Class<?> c : classes)
				ret.add(c);

		if (representationComponentClasses != null)
			for (Class<?> c : representationComponentClasses)
				ret.add(c);

		return ret.toArray(new Class<?>[ret.size()]);
	}

	@Override
	public Type getDeviceType(String id) {
		if (sock != null && sock.isConnected())
			synchronized (sock) {
				try {
					return NetworkProxy.getDeviceType(
							this.sock,
							this.classLoader != null ? this.classLoader
									: NetworkPublisherInformationProvider.class
											.getClassLoader());
				} catch (IOException e) {
					LOGGER.error("error while getting device type", e);
					sock = null;
				} catch (DeserializableException e) {
					LOGGER.error("error while getting device type", e);
				}
			}
		return super.getDeviceType(id);
	}

	@Override
	public Map<String, Object> getPropertiesMap(String id) {
		if (sock != null && sock.isConnected())
			synchronized (sock) {
				return properties;
			}
		return super.getPropertiesMap(id);
	}

	@SuppressWarnings("unchecked")
	@Override
	public Map<String, String> getDisplayName(String id) {
		if (properties == null)
			return super.getDisplayName(id);

		Object m = properties.get("display-name");

		if (m != null) {
			if (!(m instanceof Map<?, ?>))
				LOGGER.warn("display-name porperty map entry has no Map as value type");
			else
				return (Map<String, String>) m;
		}
		return super.getDisplayName(id);
	}

	@Override
	public Type[] getInterfaces(Type deviceType) {
		if (sock != null && sock.isConnected())
			synchronized (sock) {
				try {
					return NetworkProxy.getInterfaces(
							sock,
							this.classLoader != null ? this.classLoader
									: NetworkPublisherInformationProvider.class
											.getClassLoader());
				} catch (IOException e) {
					LOGGER.error("error while getting device interfaces", e);
					sock = null;
				} catch (DeserializableException e) {
					LOGGER.error("error while getting device interfaces", e);
				}
			}
		return super.getInterfaces(deviceType);
	}

	@Override
	public Tree getLocalInstanceTree(String id) {
		if (properties == null)
			return super.getLocalInstanceTree(id);

		Object e = properties.get("local-instance");
		if (e != null) {
			if (!(e instanceof Tree))
				LOGGER.warn("local-instance porperty map entry has no SExpression as value type");
			else
				return (Tree) e;
		}
		return super.getLocalInstanceTree(id);
	}

	@SuppressWarnings("unchecked")
	@Override
	public Map<String, Position> getPosition(String id) {
		if (properties == null)
			return super.getPosition(id);

		Object m = properties.get("position");
		if (m != null) {
			if (!(m instanceof Map<?, ?>))
				LOGGER.warn("display-name porperty map entry has no Map as value type");
			else
				return (Map<String, Position>) m;
		}
		return super.getPosition(id);
	}

	@Override
	public ClassLoader getInformationProviderClassLoader() {
		return (this.classLoader != null ? this.classLoader
				: this.parentClassLoader);
	}
}
